import arrow
import json
import re
import os
import traceback

from threading import Thread
from pymongo import MongoClient, UpdateOne

from conf.settings import CLOUDMONITOR_MONGODB, CLOUDMONITOR_DB, DATE_FMT
from utility.utils import get_flat_value, merge_feature, Step
from utility.sendcloud import SendCloud
from utility.yunpian import YunPian
from utility.monitor import Monitor
from utility.log import Logger

logger = Logger('packages_refresh.log').get_logger()

db = MongoClient(CLOUDMONITOR_MONGODB)[CLOUDMONITOR_DB]
# db = MongoClient(host='localhost', port=27017)['cloudmonitor']
monitor = Monitor()

recharge_limit_type = ('limit', 'period', 'max')
task_type_tuple = ('vul', 'ssl', 'securityEvent', 'ping', 'http', 'asset', 'content')

ARROW_NOW = arrow.utcnow()
# ARROW_NOW = arrow.utcnow().replace(day=17, hour=16)

DETAIL_URL = 'https://console.scanv.com'

ENABLE_MODULES = {
    # NOTE: It will be done in order.
    'domain': {
        'update_dict': {'enableMonitor': False},  # mongo update sql
        'conditions': [('enableMonitor', True)],  # check key if is enabled.
        'stop_params': {'jobId': '_id', 'taskId': '*'},  # stopping task required params.
    },
    'httpMaxTask': {
        'update_dict': {'httpSettings.{index}.enable': False},
        'conditions': [('enableMonitor', True), ('httpSettings.{index}.enable', True)],
        'stop_params': {'jobId': '_id', 'taskId': 'httpSettings.{index}.taskId'},
    },
    'sitePortrait': {
        'update_dict': {'vulSettings.enableSitePortraitTriggerMonitor': False},
        'conditions': [('enableMonitor', True), ('vulSettings.enableSitePortraitTriggerMonitor', True)],
        'stop_params': {'jobId': '', 'taskId': ''}
    }
}

NOTICE_CONF = {
    'domain': {
        'email_conf': {
            'template': 'scanvmax_asset_will_expire',
            'params': ('user', 'time', 'num1')
        },
        'sms_conf': {'template': '5238722', 'params': ('num1', 'time')},
        'conditions': [('number', True)]
    },
    'httpMaxTask': {
        'email_conf': {
            'template': 'scanvmax_http_will_expire',
            'params': ('user', 'time', 'num', 'num1')
        },
        'sms_conf': {'template': '5238764', 'params': ('num', 'num1', 'time')},
        'conditions': [('number', True)]
    },
    'sitePortrait': {
        'email_conf': {
            'template': 'scanvmax_webportrait_will_expire',
            'params': ('user', 'time', 'num1')
        },
        'sms_conf': {'template': '5238720', 'params': ('num1', 'time')},
        'conditions': [('number', True)]
    },
    'maxPage': {
        'email_conf': {
            'template': 'scanvmax_url_will_expire',
            'params': ('user', 'time', 'num', 'num1')
        },
        'sms_conf': {'template': '5238730', 'params': ('num', 'num1', 'time')},
        'conditions': [('number', True)]
    },
    'manualTask': {
        'email_conf': {
            'template': 'scanvmax_single_will_expire',
            'params': ('user', 'time', 'num', 'num1')
        },
        'sms_conf': {'template': '5238734', 'params': ('num', 'num1', 'time')},
        'conditions': [('number', True)]
    },
    'smsAlert': {
        'email_conf': {
            'template': 'scanvmax_message_will_expire',
            'params': ('user', 'time', 'num', 'num1')
        },
        'sms_conf': {'template': '5238740', 'params': ('num', 'num1', 'time')},
        'conditions': [('number', True)]
    },
    'emailAlert': {
        'email_conf': {
            'template': 'scanvmax_email_will_expire',
            'params': ('user', 'time', 'num', 'num1')
        },
        'sms_conf': {'template': '5238752', 'params': ('num', 'num1', 'time')},
        'conditions': [('number', True)]
    },
    'plan': {
        'email_conf': {
            'template': 'scanvmax_plan_will_expire',
            'params': ('user', 'edition', 'close_time')
        },
        'sms_conf': {'template': '5238700', 'params': ('edition', 'close_time')},
    }
}

EXPIRE_NOTICE_CONF = {
    'domain': {
        'email_conf': {
            'template': 'scanvmax_asset_has_expire',
            'params': ('user', 'time', 'num1', 'url')
        },
        'sms_conf': {'template': '5238724', 'params': ('num1', 'time')},
    },
    'httpMaxTask': {
        'email_conf': {
            'template': 'scanvmax_http_has_expire',
            'params': ('user', 'time', 'num', 'num1', 'num2', 'domain_list', 'url')
        },
        'sms_conf': {'template': '5238766', 'params': ('num', 'num1', 'time')},
    },
    'sitePortrait': {
        'email_conf': {
            'template': 'scanvmax_webportrait_has_expire',
            'params': ('user', 'time', 'num1', 'num2', 'domain_list', 'url')
        },
        'sms_conf': {'template': '5238714', 'params': ('num1', 'time')},
    },
    'maxPage': {
        'email_conf': {
            'template': 'scanvmax_url_has_expire',
            'params': ('user', 'time', 'num', 'num1')
        },
        'sms_conf': {'template': '5238732', 'params': ('num', 'num1', 'time')},
        'conditions': [('number', True)]
    },
    'manualTask': {
        'email_conf': {
            'template': 'scanvmax_single_has_expire',
            'params': ('user', 'time', 'num', 'num1')
        },
        'sms_conf': {'template': '5238736', 'params': ('num', 'num1', 'time')},
        'conditions': [('number', True)]
    },
    'smsAlert': {
        'email_conf': {
            'template': 'scanvmax_message_has_expire',
            'params': ('user', 'time', 'num', 'num1')
        },
        'sms_conf': {'template': '5238746', 'params': ('num', 'num1', 'time')},
        'conditions': [('number', True)]
    },
    'emailAlert': {
        'email_conf': {
            'template': 'scanvmax_email_has_expire',
            'params': ('user', 'time', 'num', 'num1')
        },
        'sms_conf': {'template': '5238758', 'params': ('num', 'num1', 'time')},
        'conditions': [('number', True)]
    },
    'plan': {
        'email_conf': {
            'template': 'scanvmax_plan_expired',
            'params': ('user', 'edition')
        },
        'sms_conf': {'template': '5238690', 'params': ('edition',)},
    }
}

CHECK_FREQ = {
    'vulMaxFreq': {'name': 'vulSettings.interval', 'enable': 'vulSettings.enable'},
    'vulMaxDepth': {'name': 'vulSettings.collect.depth', 'enable': 'vulSettings.enable'},
    'vulMaxPage': {'name': 'vulSettings.collect.maxPage', 'enable': 'vulSettings.enable'},
    'sslMaxFreq': {'name': 'sslSettings.interval', 'enable': 'vulSettings.enable'},
    'securityEventMaxFreq': {'name': 'securityEventSettings.interval', 'enable': 'securityEventSettings.enable'},
    'securityEventMaxDepth': {'name': 'securityEventSettings.collect.depth', 'enable': 'securityEventSettings.enable'},
    'securityEventMaxPage': {'name': 'securityEventSettings.collect.maxPage', 'enable': 'securityEventSettings.enable'},
    'httpMaxFreq': {'name': 'httpSettings.{index}.interval', 'enable': 'httpSettings.{index}.enable'},
    'httpMaxNode': {'name': 'httpSettings.{index}.collect.node', 'enable': 'httpSettings.{index}.enable'},
    'pingMaxFreq': {'name': 'pingSettings.{index}.interval', 'enable': 'pingSettings.{index}.enable'},
    'pingMaxNode': {'name': 'pingSettings.{index}.collect.node', 'enable': 'pingSettings.{index}.enable'},
    'assetMaxFreq': {'name': 'taskSettings.interval', 'enable': 'taskSettings.enable'},
    'contentMaxFreq': {'name': 'contentSettings.interval', 'enable': 'contentSettings.enable'},
    'contentMaxDepth': {'name': 'contentSettings.collect.depth', 'enable': 'contentSettings.enable'},
    'contentMaxPage': {'name': 'contentSettings.collect.maxPage', 'enable': 'contentSettings.enable'},
}


def get_check_params(module_dict: dict) -> (dict, dict):
    """
    check_params: {
        'httpMaxTask': {
            'update_dict': {'vulSettings': 'enableSitePortraitTriggerMonitor': False, 'enable': False},
            'conditions': [('enable', True}, ('vulSettings.enableSitePortraitTriggerMonitor': True)],
            'num': 3
        },
        module_name: {
            update_dict: dict(),
            conditions: list(),
            num: int
        }
        ..........................................
    }
    """
    check_params = dict()

    for _module, _num in module_dict.items():
        if _num == 0:
            continue
        if _num == '*':
            _num = 1073741824

        check_params[_module] = {
            'update_dict': {},
            'conditions': [],
            'close_num': _num
        }
        _setting = ENABLE_MODULES.get(_module, {})
        if _setting:
            check_params[_module]['update_dict'] = _setting.get('update_dict', {})
            check_params[_module]['conditions'] = _setting.get('conditions', {})
            check_params[_module]['stop_params'] = _setting.get('stop_params', {})

    return check_params


def is_list(obj):
    s1 = json.dumps(obj)
    if re.match(r'.*\.{index}\..*', s1):
        return True

    return False


def check_list_value(data, conditions, i):
    for cnd in conditions:
        _k_list = cnd[0].format(index=i).split('.')
        inner_data = data
        for _k in _k_list:
            if _k.isdigit():
                if (j := int(_k)) >= len(inner_data):
                    return False
                inner_data = inner_data[j]
            else:
                inner_data = inner_data.get(_k)
                if not _k:
                    return False
        if inner_data == cnd[1]:
            continue
        else:
            return False

    return True


def check_dict_value(d0: dict, conditions: list) -> bool:
    for cnd in conditions:
        _inner_data = d0
        _check_list = cnd[0].split('.')
        for _check_k in _check_list:
            _inner_data = _inner_data.get(_check_k)
            if not _inner_data:
                return False
        if not _inner_data == cnd[1]:
            return False

    return True


def stop_jobs(job_list):
    t_list = []
    for job_id in job_list:
        t_list.append(StopTaskThread(job_id))
    for t in t_list:
        t.start()
    for t in t_list:
        t.join()

    del t_list


class StopTaskThread(Thread):
    def __init__(self, job_id):
        self._job_id = job_id
        super(StopTaskThread, self).__init__()

    def run(self):
        request_args = {
            "jobId": str(self._job_id),
        }
        resp = monitor.post('/api/v2/jobs/tasks/scanner/remove', request_args)
        if not resp or resp.status_code != 200:
            logger.info(f'Remove error {resp}')


class User(Step):

    def __init__(self, o_uid, username):
        super(User, self).__init__()

        self.next_reset_time = ARROW_NOW.replace(
            day=1, hour=2, minute=0, second=0, microsecond=0).shift(months=1, hours=-8, minutes=-5).int_timestamp
        self.now_datetime = ARROW_NOW.datetime
        self.notice_day = [1, 3, 7, 10]
        self.username = username
        self.uid = o_uid
        self.balance = None
        self.balance_query = {'uid': o_uid}
        self.notice_dict = {}
        self._refresh_balances()

    def _init_1_clean_balance(self):
        collection = db['balances']
        collection_jobs = db['jobs']
        balance = self.balance
        if not balance.get('planExpireTime'):
            logger.info("Skip because of no planExpireTime.")
            self.skip = True
            return

        update_dict = {}
        for k, item in balance['balance'].items():
            if item.get('used', 0) < 0:
                update_dict['balance.{k}.used'] = 0

        # recalculate domainEnable
        update_dict['balance.domainEnable.used'] = collection_jobs.count_documents(
            {'enableMonitor': True, 'uid': self.uid})

        if update_dict:
            collection.update_one({'_id': balance['_id']}, {'$set': update_dict})
            logger.info(f"Succeed clean balance")
            self._refresh_balances()

        # notification
        if (plan_expire_time := balance['planExpireTime']) and \
                db['charges'].find_one({
                    'uid': self.uid,
                    'chargeType': 'plan',
                    'isDelete': False,
                    'expireTime': {'$exists': True}
                }) and \
                (plan := db['packages'].find_one({'_id': balance['planId']})):

            str_expire_time = plan_expire_time.strftime(DATE_FMT)
            days = (arrow.get(plan_expire_time).shift(hours=-8) - ARROW_NOW).days
            notice = {}
            if days <= 0:
                notice['expire'] = {str_expire_time: {'edition': plan['name'], 'user': self.username}}
            elif days <= self.notice_day[-1] and days in self.notice_day:
                notice['notice'] = {str_expire_time: {
                    'close_time': str_expire_time, 'edition': plan['name'], 'user': self.username
                }}

            self.notice_dict['plan'] = notice

    def _init_2_recharge_used(self):
        collection_charges = db['charges']
        pipeline = [
            {'$match': {'uid': self.uid, 'isDelete': False, 'limitType': {'$in': recharge_limit_type}}},
            {'$sort': {'expireTime': 1}},
            {'$sort': {'limitType': -1}},
            {'$group': {
                '_id': '$funcType',
                'charges': {'$addToSet': {'_id': '$_id', 'used': '$used', 'total': '$total'}}
            }}
        ]

        update_list = []
        for item in collection_charges.aggregate(pipeline):
            func_type = item['_id']
            charges = item['charges']
            is_exhaust = False
            balance_func = self.balance['balance'].get(func_type, {})
            _used = balance_func.get('used', 0)

            for charge in charges:
                if is_exhaust:
                    update_list.append(UpdateOne(
                        {'_id': charge.get('_id')}, {'$set': {'used': 0, 'updateTime': self.now_datetime}}))
                    continue
                if (r := _used - charge.get('total', 0)) <= 0:
                    update_list.append(UpdateOne(
                        {'_id': charge.get('_id')}, {'$set': {'used': _used, 'updateTime': self.now_datetime}}))
                    is_exhaust = True
                else:
                    update_list.append(UpdateOne(
                        {'_id': charge.get('_id')}, {'$set': {'used': charge.get('total', 0), 'updateTime': self.now_datetime}}))
                    _used = r

        if update_list:
            collection_charges.bulk_write(update_list)
            logger.info("Succeed recalculated used of charges.")

    def _init_3_recalculate_balance(self):
        collection_charges = db['charges']
        collection_balances = db['balances']

        func_type_total_dict = {}
        for charge in collection_charges.find({'isDelete': False, 'uid': self.uid}):
            func_type = charge.get('funcType')
            func_type_total_dict[func_type] = func_type_total_dict.get(func_type, 0) + charge.get('total', 0)

        balance_update_dict = {}
        for func_type, item in self.balance.get('balance', {}).items():
            balance_update_dict[f"balance.{func_type}.total"] = func_type_total_dict.get(func_type, 0)

        if balance_update_dict:
            collection_balances.update_one({'_id': self.balance['_id']}, {'$set': balance_update_dict})
            logger.info("Succeed to recalculate total fo balance.")
        self._refresh_balances()

    def _step_1_update_charges(self):
        collection_charges = db['charges']
        collection_balances = db['balances']

        balance_update_dict = {'$inc': {}, '$set': {}}
        charges_update_list = []

        for charge in collection_charges.find({'uid': self.uid, 'isDelete': False}):
            _limit_type = charge['limitType']
            _func_type = charge['funcType']
            _used = charge.get('used', 0)
            _total = charge.get('total', 0)
            _time = charge.get('time', 0)
            _charge_type = charge.get('chargeType')
            _expire_time = charge.get('expireTime')
            if not _expire_time:
                if _charge_type == 'plan':
                    _expire_time = self.balance.get('planExpireTime')
                else:
                    continue

            # notice
            if _charge_type == 'additionPackage':
                self._set_notice(NOTICE_CONF.get(_func_type), _expire_time, charge)

            # expired
            if arrow.get(_expire_time).shift(hours=-8).datetime < self.now_datetime:
                if _limit_type in ('limit', 'max'):
                    _temp = balance_update_dict['$inc'].get(f"balance.{_func_type}.total", 0)
                    balance_update_dict['$inc'][f"balance.{_func_type}.total"] = _temp - _total
                if _limit_type in ('period', 'max'):
                    _temp = balance_update_dict['$inc'].get(f"balance.{_func_type}.used", 0)
                    balance_update_dict['$inc'][f"balance.{_func_type}.used"] = _temp - _used
                charges_update_list.append(UpdateOne({'_id': charge['_id']}, {'$set': {'isDelete': True}}))
                continue

            # reset
            if _limit_type == 'period' and arrow.get(_time) < self.now_datetime:
                charges_update_list.append(
                    UpdateOne({'_id': charge['_id']}, {'$set': {'used': 0, 'time': self.next_reset_time}}))
                _temp = balance_update_dict["$inc"].get(f"balance.{_func_type}.used", 0)
                balance_update_dict['$inc'][f"balance.{_func_type}.used"] = _temp - _used
            # 过期了的数据，需要标记删除
            if _limit_type == "exp" and arrow.get(_time) < self.now_datetime:
                charges_update_list.append(
                    UpdateOne({'_id': charge['_id']},
                              {'$set': {'used': 0, 'time': 0, 'total': 0, 'number': 0, 'isDelete': True}}))

        if charges_update_list:
            collection_charges.bulk_write(charges_update_list)
            logger.info("Succeed update charges.")

        for k, item in self.balance["balance"].items():
            # 按照持续时间结算的模块, 过期了重置数据
            if item.get("limitType") == "exp" and item.get("time") < ARROW_NOW.timestamp():
                balance_update_dict['$set'][f"balance.{k}"] = \
                    {'time': 0, 'taskType': '', 'limitType': 'exp', 'total': 0, 'used': 0, 'number': 0}
                balance_update_dict['$set'][f"feature.{k}"] = False

        balance_update_dict = {k: v for k, v in balance_update_dict.items() if v}
        if balance_update_dict:
            collection_balances.update_one({'_id': self.balance['_id']}, balance_update_dict)
            logger.info("Succeed update balance")

        self._refresh_balances()

    def _step_2_update_balance(self):
        days = (arrow.get(self.balance['planExpireTime']).shift(hours=-8) - ARROW_NOW).days
        # 过期一天后再清除任务
        if days >= -1:
            return
        job_list = db.asset_tasks.distinct(
            'jobId',
            {
                'triggerType': 'schedule',
                'uid': self.uid,
                'enableMonitor': True,
                # 'taskGroupType': {'$ne': 'host'}  # host主机监测批量移除任务存在缺陷
            }
        )
        if job_list:
            logger.info(f"Process remove {len(job_list)} jobs.")
            stop_jobs(job_list)
            logger.info(f"Removed jobs {job_list} .")

    def _step_3_send_alert(self):
        # 先只通知套餐到期
        if self.notice_dict.get("plan"):
            AlertReceivers(self.username, self.uid, {"plan": self.notice_dict.get("plan")}, {}, {}).travel()

    # def _step_3_calculate_feature(self):
    #     collection_charges = db['charges']
    #     collection_packages = db['packages']
    #     collection_balances = db['balances']
    #
    #     pipeline = [
    #         {'$match': {'uid': self.uid, 'isDelete': False}},
    #         {'$group': {'_id': '$packId'}}
    #     ]
    #
    #     id_list = [item['_id'] for item in collection_charges.aggregate(pipeline)]
    #
    #     feature = {}
    #     for item in collection_packages.find({'_id': {'$in': id_list}}):
    #         if not feature:
    #             feature = item.get('feature', {})
    #         else:
    #             feature = merge_feature(feature, item.get('feature', {}))
    #
    #     collection_balances.update_one({'_id': self.balance['_id']}, {'$set': {'feature': feature}})
    #     self._refresh_balances()

    # def _step_4_stop_unlimited_task(self):
    #     feature = self.balance['feature']
    #
    #     collection_jobs = db['jobs']
    #     collection_assets = db['assets']
    #
    #     jobs = collection_jobs.find({'uid': self.uid})
    #
    #     # check feature
    #     stop_task_type = []
    #     for func_type, v in feature.items():
    #         for task_type in task_type_tuple:
    #             if func_type == f"{task_type}Task" and not v:
    #                 stop_task_type.append(task_type)
    #     balance_freq = {}
    #
    #     # check balance
    #     for func_type, item in self.balance['balance'].items():
    #         if item.get('limitType') == 'frequency':
    #             balance_freq[func_type] = item.get('total')
    #
    #     jobs_update_list = []
    #     enabled_jobs_list = []
    #     for job in jobs:
    #         if not job.get('enableMonitor'):
    #             continue
    #         enabled_jobs_list.append(job['_id'])
    #         for task_type in task_type_tuple:
    #             _setting_key = f"{task_type}Settings"
    #             if not (_settings := job.get(_setting_key)):
    #                 continue
    #
    #             # stop feature limit
    #             if task_type in stop_task_type:
    #                 if isinstance(_settings, list):
    #                     jobs_update_list.append({
    #                         'update_dict': UpdateOne({'_id': job['_id']}, {'$set': {f"{_setting_key}.$[].enable": False}}),
    #                         'stop_dict': {'taskId': '*', 'jobId': str(job['_id'])}
    #                     })
    #                 elif _settings.get('enable'):
    #                     jobs_update_list.append({
    #                         'update_dict': UpdateOne({'_id': job['_id']}, {'$set': {f"{_setting_key}.enable": False}}),
    #                         'stop_dict': {'taskId': '*', 'jobId': str(job['_id'])}
    #                     })
    #                 continue
    #
    #             # stop over freq
    #             # _balance_freq = balance_freq.get(f"{task_type}MaxFreq")
    #             # _balance_node = ''
    #             # _balance_depth = ''
    #             # if isinstance(_settings, list):
    #             #     for _list_setting in _settings:
    #             #
    #             #     jobs_update_list.append({
    #             #         'update_dict': UpdateOne({'_id': job['_id']}, {'$set': {f"{_setting_key}.$[].enable": False}}),
    #             #         'stop_dict': {'taskId': '*', 'jobId': str(job['_id'])}
    #             #     })
    #             # elif _settings.get('enable'):
    #             #     jobs_update_list.append({
    #             #         'update_dict': UpdateOne({'_id': job['_id']}, {'$set': {f"{_setting_key}.enable": False}}),
    #             #         'stop_dict': {'taskId': '*', 'jobId': str(job['_id'])}
    #             #     })
    #
    #     assets_update_list = []
    #     for asset in collection_assets.find({'uid': self.uid, 'jobId': enabled_jobs_list}):
    #         assets_update_list.append({
    #             'update_dict': UpdateOne({'_id': asset['jobId']}, {'$set': {"taskSettings.enable": False}}),
    #             'stop_dict': {'taskId': '*', 'jobId': str(asset['jobId'])}
    #         })
    #
    #     stop_jobs(collection_jobs, jobs_update_list)
    #     stop_jobs(collection_assets, assets_update_list)
    #     logger.info(f"Stopped unlimited task, job_update_list: {jobs_update_list}, 'assets_update_list': {assets_update_list}")

    def _set_notice(self, notice_conf, expire_time, charge):
        func_type = charge.get('funcType')
        notice = self.notice_dict.setdefault(func_type, {})
        if notice_conf:
            for _cnd in notice_conf.get('conditions', []):
                _k0 = charge.get(_cnd[0])
                if isinstance(_cnd[1], bool):
                    bool_exp = bool(_k0) == bool(_cnd[1])
                else:
                    bool_exp = _k0 == _cnd[1]

                if not bool_exp:
                    return None

            days = (arrow.get(expire_time).shift(hours=-8) - ARROW_NOW).days
            expire_date = expire_time.strftime(DATE_FMT)
            if days <= 0:
                notice = notice.setdefault('expire', {})
            elif days <= self.notice_day[-1] and days in self.notice_day:
                notice = notice.setdefault('notice', {})
            else:
                return

            info = notice.get(expire_date, {})
            if info:
                info['num1'] += int(charge.get('total', 0))
                info['num'] += int(charge.get('number', 0))
            else:
                info = {
                    'time': expire_date,
                    'num': 1,
                    'num1': charge.get('total', 0),
                    'num2': 0,
                    'domain_list': '暂无',
                    'url': DETAIL_URL
                }
            notice[expire_date] = info

    def _refresh_balances(self):
        self.balance = db['balances'].find_one(self.balance_query)


class AlertReceivers(object):
    def __init__(self, username, uid, notice_dict: dict, affect_urls: dict, close_dict: dict):
        collection_receivers = db['alertreceivers']
        self.username = username
        self.uid = uid

        self.notice_dict = notice_dict
        self.affect_urls = affect_urls
        self.close_dict = close_dict
        self.receiver = collection_receivers.find_one({
            'enable': True,
            'isDefault': True,
            'uid': self.uid,
            '$or': [{'phoneVerified': True}, {'emailVerified': True}]
        })
        self.sendcloud = SendCloud()
        self.yunpian = YunPian()

    def travel(self):
        if not self.receiver:
            return
        email = self.receiver.get('email') if self.receiver.get('emailVerified', False) else None
        phone = self.receiver.get('phone') if self.receiver.get('phoneVerified', False) else None
        username = self.username
        for _module_name, _info in self.notice_dict.items():
            for _, __params in _info.get('notice', {}).items():
                self._send_notice_info(_module_name, username, email, phone, __params)

            for _, __params in _info.get('expire', {}).items():
                if __domain_list := self.affect_urls.get(_module_name, []):
                    __params['domain_list'] = self._trans_domain_list(__domain_list)
                if __close_count := self.close_dict.get(self.uid, {}).get(_module_name, 0):
                    __params['num2'] = __close_count

                self._send_expire_info(_module_name, username, email, phone,  __params)

    def _send_notice_info(self, module_name, username, email, phone, params):
        conf = NOTICE_CONF.get(module_name, {})
        self._send_info(conf, username, email, phone, params)
        logger.info(f"Succeed send '{module_name}' notification message.")

    def _send_expire_info(self, module_name, username, email, phone, params):
        conf = EXPIRE_NOTICE_CONF.get(module_name, {})
        self._send_info(conf, username, email, phone, params)
        logger.info(f"Succeed send '{module_name}' expired message.")

    def _send_info(self, conf, username, email, phone, data):
        email_conf = conf.get('email_conf')
        sms_conf = conf.get('sms_conf')

        # send email
        if email:
            data['user'] = username
            params = {}
            for k in email_conf.get('params'):
                params[k] = data.get(k, '')

            args = {
                'to': [email],
                'sub': {f'%{k}%': [v] for k, v in params.items()}
            }
            self.sendcloud.sendmail(email_conf.get('template'), args)

        # send sms
        if phone:
            params = {}
            for k in sms_conf.get('params'):
                params[k] = data.get(k, '')

            args = '&'.join([f"#{k}#={v}" for k, v in params.items()])
            self.yunpian.tpl_send(phone, sms_conf.get('template'), args)

    @staticmethod
    def _trans_domain_list(domain_list):
        t_body = """
        <div style="width: 500px;margin: 0 auto; border: 1px solid #e7eaef;boder-radius: 2px;padding:10px 0">
            <div style="max-height: 100px; overflow-y:auto">
                <table>
	                <tbody>
	                {tr}
	                </tbody>
                </table>
            </div>
        </div>
        """
        tr = """
        <tr>
            <td style="padding:0 10px">{name}</td>
            <td style="padding:0 10px">{url}</td>
        </tr>
        """

        s0 = ""
        for domain in domain_list:
            s0 = f"{s0}{tr.format(name=domain[0], url=domain[1])}"

        return t_body.format(tr=s0).replace('\n', '')


def main():
    total = db['users'].count_documents({})
    i = 0
    begin = arrow.utcnow()

    logger.info(f"Start {os.path.basename(__file__)}\n {total} users\n")

    for user in db['users'].find():
        username = user.get('username')
        uid = user.get('_id')
        logger.info(f"Init username: {username}, uid: {uid}")
        try:
            User(user.get('_id'), username).run()
        except Exception as e:
            traceback.print_exc()
            logger.exception(e)

        i += 1
        progress = f"{i/total*100}"[:5]
        logger.info(f"Finish [{progress}%]")

    logger.info(f"Completed {os.path.basename(__file__)}, costs {arrow.utcnow()-begin}")


if __name__ == '__main__':
    main()

    # from bson import ObjectId
    # user = db['users'].find_one({'_id': ObjectId("62fde0641f1b190012421a29")})
    #
    # User(user['_id'], user['username']).run()

